/**
 * Copyright (C) 2006 OQube 
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *  
 * author: Arnaud Bailly - abailly AT oqube DOT com
 * created: 14 juin 2006
 */
package speculoos.jndi.mappers;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;

import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;

import org.apache.commons.collections.MapUtils;

import speculoos.core.Mapper;
import speculoos.core.MapperException;
import speculoos.utils.TypeHelper;

/**
 * This mapper creates an {@see javax.naming.directory.Attributes} objcet from a
 * Java object according to some mapping rules defined in a map.
 * Note that everything gets converted to String using method {@see #convert(Object,Object,Class)}
 * method. By default, this method returns the result of invoking 
 * {@see java.lang.Object#toString()}.
 *  
 * @author nono
 * 
 */
public class AttributesFromInput implements Mapper {

	/*
	 * a map from java names to ldap names
	 */
	private Map outputMap;

	private boolean caseIgnore = true;

	/**
	 * construct a filter from given map.
	 * 
	 * @param map
	 *            the (inverted) map from LDAP names to Java names.
	 */
	public AttributesFromInput(Map map) {
		this.outputMap = MapUtils.invertMap(map);
	}

	/**
	 * This method uses reflection through the {@see fr.lifl.utils.TypeHelper}
	 * class. On successful completion, it returns an instance of Attributes
	 * that may later be used for input to other mappers.
	 * 
	 * @see speculoos.core.Mapper#map(java.lang.Object, java.util.Map)
	 */
	public Object map(Object input, Map param) throws MapperException {
		TypeHelper th = TypeHelper.instance;
		BeanInfo info;
		try {
			info = Introspector.getBeanInfo(input.getClass());
		} catch (IntrospectionException e) {
			throw new MapperException(getName()
					+ ": cannot access fields info from Introspector", e);
		}
		Attributes attributes = new BasicAttributes(isCaseIgnore());
		PropertyDescriptor[] props = info.getPropertyDescriptors();
		for (int i = 0; i < props.length; i++) {
			PropertyDescriptor p = props[i];
			String n = (String) outputMap.get(p.getName());
			if (n == null)
				continue;
			Attribute attribute = new BasicAttribute(n);
			Object o = th.get(input, p.getName());
			/* special handling for collections */
			if (o != null) {
				if (o instanceof Collection) {
					for (Iterator it = ((Collection) o).iterator(); it
							.hasNext();)
						attribute.add(convert(p,it.next()));
				} else {
					attribute.add(convert(p,o));
				}
				if (attribute.size() > 0 ) {
					// do not add a multi valued empty attribute
					attributes.put(attribute);
				}
			}
		}
		return attributes;
	}

	/**
	 * Convert the value of the given property to something suitable
	 * for input to a LDAP query.
	 * By default, this method returns the string representation of the
	 * given object.
	 *  
	 * @param p the property to convert.
	 * @param o the value of property to convert.
	 * @return the converted value.
	 */
	public Object convert(PropertyDescriptor p, Object o) {
		return (o == null) ? null : o.toString();
	}

	/*
	 * @see speculoos.core.Mapper#getName()
	 */
	public String getName() {
		return "AttributesFromInput";
	}

	/**
	 * @return Returns the outputMap.
	 */
	public Map getOutputMap() {
		return outputMap;
	}

	/**
	 * Make the output map used for this filter as the inverse of the given map.
	 * <strong>NOTE</strong>: standard maps have LDAP names as key and Java
	 * names as values.
	 * 
	 * @param outputMap
	 *            The outputMap to set. It must be bijective and not contain
	 *            null keys or values.
	 */
	public void setOutputMap(Map outputMap) {
		this.outputMap = MapUtils.invertMap(outputMap);
	}

	/**
	 * @return Returns the caseIgnore.
	 */
	public boolean isCaseIgnore() {
		return caseIgnore;
	}

	/**
	 * @param caseIgnore
	 *            The caseIgnore to set.
	 */
	public void setCaseIgnore(boolean caseIgnore) {
		this.caseIgnore = caseIgnore;
	}

	/**
	 * Factory method for creating AttributesFromInput instances.
	 * <p>
	 * This factory method first tries to load an instance of 
	 * this class that is specifically tailored to match the given class.
	 * By convention, this class is looked up in the current ClassLoader 
	 * with package corresponding to <code>cls</code>'s package 
	 * and with name <code>AttributesFromXXX</code> where <code>XXX</code> is 
	 * the simple name of the given class object.
	 * </p>
	 * <p>
	 * <strong>NOTE:</strong> this method assumes that there is only one
	 * transformer per-class, the map argument is ignored while looking 
	 * for a class.
	 * </p>
	 * @param cls the Class instance to look transformer for.
	 * @param map the Map from LDAP name to Java names.
	 * @return an initialized instance of this class.
	 */
	public static AttributesFromInput create(Class cls, Map map) {
		if(map == null)
			throw new IllegalArgumentException("Cannot create attributes map from null map");
		if(cls == null)
			return new AttributesFromInput(map);
		/* compute class name */
		String cln = cls.getPackage().getName() + ".AttributesFrom"+cls.getName().substring(cls.getPackage().getName().length());		
		/* load it */
		try {
			Class attrs = Class.forName(cln);
			return (AttributesFromInput) attrs.newInstance();
		}catch(Exception e) {
			return new AttributesFromInput(map);
		}
	}

}
